/*
Copyright (C) 2015 Marien Raat

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <perlinNoise.h>
#include <normalizer.h>
#include <math.h>
#include <iostream>

PerlinNoiseGenerator::PerlinNoiseGenerator(std::mt19937 *mt)
{
    this->mt = mt;
    directionalDistribution = new std::uniform_real_distribution
        <double>(0.0, 2 * PI);
    firstGradientMap = true;
    firstPerlinNoiseMap = true;
}

double *PerlinNoiseGenerator::generatePerlinNoiseMap(
    int width,
    int height,
    double featureSize,
    bool overwriteOldGradientMap,
    bool overwriteOldPerlinNoiseMap)
{
    int gradientWidth = (int) ((((double) width) / featureSize)) + 1,
        gradientHeight = (int) ((((double) height) / featureSize)) +
        1;

    generateGradientMap(gradientWidth, gradientHeight,
                        overwriteOldGradientMap);

    generatePerlinNoise(featureSize, overwriteOldPerlinNoiseMap);

    return this->perlinNoiseMap;
}

void PerlinNoiseGenerator::generateGradientMap(
    int width,
    int height,
    bool overwriteOldGradientMap)
{
    // Allocating new space and deleting if necessary
    if (firstGradientMap || !overwriteOldGradientMap) {
        gradientMap = new double[width * height * 2];
    } else if (width != gradientMapWidth ||
               height != gradientMapHeight) {
        delete gradientMap;
        gradientMap = new double[width * height * 2];
    }

    firstGradientMap = false;
    gradientMapWidth = width;
    gradientMapHeight = height;

    // Generate a random gradient vector with a length of 1 for every
    // point.
    for (int i = 0; i < width * height; i++) {
        // The angle of the vector in the range [0, 2 * PI]
        double angle = (*directionalDistribution)(*mt);
        gradientMap[(2 * i) + 0] = cos(angle); // The x-component
        gradientMap[(2 * i) + 1] = sin(angle); // The y-component
    }
}

void PerlinNoiseGenerator::generatePerlinNoise(
    double pointsPerCell,
    bool overwriteOldPerlinNoiseMap)
{
    // A gradient needs to have been generated to generate the perlin
    // noise
    if (firstGradientMap) return;

    // Calculate the needed perlin map size
    int width = (int) (pointsPerCell * ((double) gradientMapWidth - 1)),
        height = (int) (pointsPerCell * ((double) gradientMapHeight - 1));

    // Allocating new space and deleting if necessary
    if (firstPerlinNoiseMap || !overwriteOldPerlinNoiseMap) {
        perlinNoiseMap = new double[width * height];
    } else if (width != perlinNoiseMapWidth ||
               height != perlinNoiseMapHeight) {
        perlinNoiseMap = new double[width * height];
    }

    firstPerlinNoiseMap = false;
    perlinNoiseMapWidth = width;
    perlinNoiseMapHeight = height;
    
    // The coordinates to start from
    double startX = 0.5 / pointsPerCell,
        startY = 0.5 / pointsPerCell;
    for (int i = 0; i < width * height; i++) {
        double x = startX + ((double) (i % width)) / pointsPerCell,
            y = startY + ((double) (i / width)) / pointsPerCell;

        perlinNoiseMap[i] = getPerlinNoiseValue(x, y);
    }

    Normalizer<double>::normalize(perlinNoiseMap, perlinNoiseMap,
                                  width * height, 0.0, 1.0);
}

double PerlinNoiseGenerator::getPerlinNoiseValue(double x, double y)
{
    // Find surrounding grid coordinates
    // The lower coordinates
    int lowerX = (x >= 0.0 ? (int) x : (int) x - 1),
        lowerY = (y >= 0.0 ? (int) y : (int) y - 1);
    // The higher coordinates
    int higherX = lowerX + 1,
        higherY = lowerY + 1;

    // Dot products
    double topLeft = getGridGradientDotProduct(x, y, lowerX, lowerY),
        topRight = getGridGradientDotProduct(x, y, higherX, lowerY),
        bottomLeft = getGridGradientDotProduct(x, y, lowerX, higherY),
        bottomRight = getGridGradientDotProduct(x, y, higherX
                                                , higherY);

    // Interpolation
    // The distances to the lower coordinates
    double dx = x - (double) lowerX,
        dy = y - (double) lowerY;

    double interpolationValueX = getInterpolationSCurve(dx),
        interpolationValueY = getInterpolationSCurve(dy);
    
    double top = getLinearInterpolation(topRight, topLeft,
                                        interpolationValueX),
        bottom = getLinearInterpolation(bottomRight, bottomLeft,
                                        interpolationValueX);

    return getLinearInterpolation(bottom, top,
                                  interpolationValueY);
}

double PerlinNoiseGenerator::getGridGradientDotProduct(
    double pointX,
    double pointY,
    int gridX,
    int gridY)
{
    // Calculate the distance vector
    double dx = pointX - (double) gridX,
        dy = pointY - (double) gridY;

    // Returns the dot-product. Dot-product for 2-dimensional vectors
    // is v1.x * v2.x + v1.y * v2.y.
    return ((dx * gradientMap[2 * (gridX + gridY *
                                   gradientMapWidth) + 0]) +
            (dy * gradientMap[2 * (gridX + gridY *
                                   gradientMapWidth) + 1]));
}

double PerlinNoiseGenerator::getLinearInterpolation(
    double v1,
    double v2,
    double weight)
{
    return (weight * v1) + ((1.0 - weight) * v2);
}

double PerlinNoiseGenerator::getInterpolationSCurve(double distance)
{
    // 6 * (distance ^ 5) - 15 * (distance ^ 4) + 10 * (distance ^ 3)
    // Curve chosen for 0 first and second derivative
    // See: http://mrl.nyu.edu/~perlin/paper445.pdf
    return 6 * pow(distance, 5) - 15 * pow(distance, 4) +
        10 * pow(distance, 3);
}
